## Абстрактные классы и методы

Обычно классы отражают некоторые объекты окружающей действительности. 

Но иногда нам приходится работать с сущностями, которые не имеют конкретного воплощения.

Например, сущность `"животное"`. 

Есть конкретные животные - кошка, собака и так далее, но животное как таковое не имеет конк

ретного воплощения. Или сущность `"геометрическая фигура"`. 

Есть прямоугольник, квадрат, круг, треугольник, но сама по себе геометрическая фигура также не имеет конкретного воплощения. 

И обычно для описания подобных сущностей применяются абстрактные классы.

В языке Python все инструменты для создания абстрактных классов определены в специальном модуле abc, который надо дополнительно подключать в приложении

> import abc

Ключевыми компонентами этого модуля является класс ABC и аннотация `@abstractmethod`. 

Класс ABC упрощает создание абстрактного класса, и все определяемые абстрактные классы наследуются от этого класса. 

Аннотация `@abstractmethod` предназначеня для создания абстрактного метода.

Абстрактные классы определяются как обычные классы за тем исключением, что они наследуются от класса ABC из модуля abc. Например, определим абстрактный класс геометрической фигуры:

```python
import abc
class Shape(abc.ABC):
    pass
```

Как правило, абстрактные классы объявляют некоторый общий функционал для классов наследников. 

Причем некоторый функционал может не иметь никакой реализации - его реализацию должны определить классы-наследники. 

Подобный функционал оформляется в классе в виде абстрактных методов. Например, класс геометрической фигуры может иметь методы вычисления периметра, площади и т.д. 

Мы не можем определить общую формулу для вычисления площади всех фигур - для каждой конкретной фигуры принцип вычисления площади может отличаться. 

Поэтому в классе фигуры мы можем определить метод вычисления площади как абстрактный. Для этого применяется аннотация `@abstractmethod` из модуля `abc`:

```python
import abc
class Shape(abc.ABC):
    @abc.abstractmethod 
    def area (self): pass       # площадь фигуры

```

В данном случае метод `area()` определен как абстрактный. 

Так как ему не нужен конкретный функционал, в нем вызывается оператор `pass`

Стоит отметить, что мы не можем напрямую создать объект абстрактного класса с абстрактными методами, используя его конструктор:

```python
import abc
class Shape(abc.ABC):
    @abc.abstractmethod 
    def area (self): pass       # площадь фигуры
 
shape = Shape()     # ! Ошибка - так нельзя
print(shape)
```

Классы-наследники должны реализовать все абстрактные методы абстрактного класса. 

Например, определим класс прямоугольника:

```python
import abc
class Shape(abc.ABC):
    @abc.abstractmethod 
    def area (self): pass       # площадь фигуры
 
# класс прямоугольника 
class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def area (self): return self.width * self.height
     
rect = Rectangle(30, 50)
print("Rectangle area:", rect.area())   # Rectangle area: 1500 
```

Здесь класс прямоугольника `Rectangle` принимает через конструктор ширину и высоту и использует их для вычисления площади в методе `area()`.

Подобным образом можно определить и другие типы фигур. 

Например, добавим класс круга:

```python
import abc
class Shape(abc.ABC):
    @abc.abstractmethod 
    def area (self): pass       # площадь фигуры
 
# класс прямоугольника 
class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def area (self): return self.width * self.height
     
# класс круга 
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    def area (self): return self.radius * self.radius * 3.14
     
 
def print_area(shape):
    print("Area:", shape.area())
     
 
rect = Rectangle(30, 50)
circle = Circle(30)
print_area(rect)        # Area: 1500
print_area(circle)      # Area: 2826.0
```

В данном случае для вывода площади фигуры определена функция `print_area`, которая принимает любую фигуру.

При этом абстрактные классы также могут определять конструктор, атрибуты, неабстрактные методы, которые также могут применяться в классах-наследниках:

```python
import abc
class Shape(abc.ABC):
    def __init__(self, x, y):
        self.x = x
        self.y = y 
         
    @abc.abstractmethod     
    def area (self): pass       # абстрактны метод
     
    def print_point(self):          # неабстрактный метод
        print("X:", self.x, "\tY:", self.y)
 
# класс прямоугольника 
class Rectangle(Shape):
    def __init__(self, x, y, width, height):
        super().__init__(x, y)
        self.width = width
        self.height = height
    def area (self): return self.width * self.height
     
 
rect = Rectangle(10, 20, 100, 100)
rect.print_point()      # X: 10   Y: 20
```

Здесь абстрактный класс `Shape` через конструктор принимает координаты X и Y для точки, относительно которой создается фигура (например, для прямоугольника это могут быть координаты верхнего левого угла, для круга - центр). 

И также определен неабстрактный метод `print_point`, который выводит координаты точки на консоль.